#!/usr/bin/env python

""" Netevent code """

import logging
import heapq
import time
import os
import fcntl
import datetime
import select
import errno

# Make the socket nonblocking
def SetNonBlock(fd):
    flags = fcntl.fcntl(fd, fcntl.F_GETFD)
    fcntl.fcntl(fd, fcntl.F_SETFD, flags | os.O_NONBLOCK) 

class Waker(object):
    def __init__(self):
        # r, w are file descriptors.
        r, w = os.pipe()
        SetNonBlock(r)
        SetNonBlock(w)
        self.reader = os.fdopen(r, "rb", 0) # no buffering
        self.writer = os.fdopen(w, "wb", 0)
    
    def Fileno(self):
        return self.reader.fileno()

    def Wake(self):
        try:
            self.writer.write(r"x")
        except IOError:
            pass

    def Consume(self):
        try:
            while True:
                result = self.reader.read()
                if not result: break
        except IOError:
            pass
    
    def Close(self):
        self.reader.close()
        self.writer.close()
                

class Timeout(object):

    __slots__ = ['deadline', 'callback']

    def __init__(self, deadline, callback):
        if isinstance(deadline, (int, long, float)):
            self.deadline = deadline      
        elif isinstance(deadline, datetime.timedelta):
            self.deadline = time.time() + TimedeltaToSeconds(deadline) # second
        self.callback = callback
    
    def __lt__(self, other):
        return (self.deadline < other.deadline)
    
    @staticmethod 
    def TimedeltaToSeconds(td):
        return ((td.microseconds + td.days*24*3600*10**6+ td.seconds*10**6) /
                float(10**6))
            

# Use this in single thread.
# *Not* thread-safe
class NetEventLoop(object):

    _ERROR = select.EPOLLERR

    def __init__(self):
        self._poll = select.epoll()
        self._callbacks = {}
        self._timeouts = []
        self._waker = Waker()

        self._stop = False

        # Add waker event.
        self.AddEvents(self._waker.Fileno(),
                lambda fd, events: self._waker.Consume(), select.EPOLLIN)
    
    @staticmethod
    def Instance():
        if not hasattr(NetEventLoop, '_instance'):
            NetEventLoop._instance = NetEventLoop()
        return NetEventLoop._instance

    def AddEvents(self, fd, callback, events):
        """ Callback :
            def YourCallback(fd, events):
                ....
        """
        logging.debug("add event fd %d", fd)
        self._callbacks[fd] = callback 
        self._poll.register(fd, events | self._ERROR)

    def UpdateEvents(self, fd, events):
        self._poll.modify(fd, events | self._ERROR)

    def CancelEvents(self, fd):
        callback = self._callbacks.pop(fd,None)
        if callback is None:
            logging.error("Cancel events for nonexist fd %d", fd, exc_info=True)
            return

        self._poll.unregister(fd)

    def AddTimeout(self, deadline, callback):
        """ Timeout callback:
            def YourTimeoutCallback():
                ...
        """
        timeout = Timeout(deadline, callback)
        heapq.heappush(self._timeouts, timeout)

    def CancelTimeout(self, timeout):
        timeout.callback = None

    def StopLoop(self):
        self._stop = True       
        self._waker.Wake()

    def StartLoop(self):
        while True:
            if self._stop: break

            poll_timeout = 0.2 # seconds
            
            if self._timeouts:
                now = time.time()
                while self._timeouts:
                    if self._timeouts[0].callback is None:
                        # canceled 
                        heapq.heappop(self._timeouts)
                    elif time._timeouts[0].deadline <= now:
                        timeout = heapq.heappop(self._timeouts)
                        self._RunCallback(timeout.callback)
                    else:
                        poll_timeout = min(timeout.deadline-now, poll_timeout)
                        break

            try:
                fd_events = self._poll.poll(poll_timeout)
            except Exception, e:
                if (getattr(e, 'errno', None) == errno.EINTR or
                    (isinstance(getattr(e, 'args', None), tuple) and 
                        len(e.args) == 2 and e.args[0] == errno.EINTR)):
                    continue
                else:
                    raise

            for fd, event in fd_events:
                try:
                    self._callbacks[fd](fd, event)
                except Exception, e:
                    if (getattr(e, 'errno', None) == errno.EPIPE or
                            (isinstance(getattr(e, 'args', None), tuple) and
                                len(e.args) == 2 and e.args[0] == errno.EPIPE)):
                        # Client close the connecton when we want to write
                        # something.
                        logging.info("Client has closen the connection in fd %d", fd)
                        pass  
                    else:
                        logging.error("Exception in I/O callback for fd %d",fd,exc_info=True)  
                   

    def _RunCallback(self, callback):
        try:
            callback()
        except Exception:
            self._HandleCallbackExp()

    def _HandleCallbackExp(self):
        logging.error("Exception when callback %r", callback, exc_info=True)
